import { context, roles as rolesCore } from "@budibase/backend-core"
import {
  ContextUser,
  ContextUserMetadata,
  Database,
  isSSOUser,
  UserBindings,
  UserMetadata,
} from "@budibase/types"
import isEqual from "lodash/isEqual"
import {
  generateUserMetadataID,
  getGlobalIDFromUserMetadataID,
  getUserMetadataParams,
  InternalTables,
} from "../../db/utils"
import { getGlobalUsers } from "../../utilities/global"

export function combineMetadataAndUser(
  user: ContextUser,
  metadata: UserMetadata | UserMetadata[]
): ContextUserMetadata | null {
  const metadataId = generateUserMetadataID(user._id!)
  const found = Array.isArray(metadata)
    ? metadata.find(doc => doc._id === metadataId)
    : metadata
  // skip users with no access
  if (
    user.roleId == null ||
    user.roleId === rolesCore.BUILTIN_ROLE_IDS.PUBLIC
  ) {
    // If it exists and it should not, we must remove it
    if (found?._id) {
      return { ...found, _deleted: true }
    }
    return null
  }
  delete user._rev
  const newDoc = {
    ...user,
    _id: metadataId,
    tableId: InternalTables.USER_METADATA,
  }
  // copy rev over for the purposes of equality check
  if (found) {
    newDoc._rev = found._rev
    newDoc.createdAt = found.createdAt
    newDoc.updatedAt = found.updatedAt
  }
  // clear fields that shouldn't be in metadata
  delete newDoc.password
  delete newDoc.forceResetPassword
  delete newDoc.roles
  if (found == null || !isEqual(newDoc, found)) {
    return {
      ...found,
      ...newDoc,
      createdAt: found?.createdAt ?? (new Date().toISOString() as any),
      updatedAt: new Date().toISOString(),
    }
  }
  return null
}

export async function rawUserMetadata(db?: Database): Promise<UserMetadata[]> {
  if (!db) {
    db = context.getWorkspaceDB()
  }
  return (
    await db.allDocs<UserMetadata>(
      getUserMetadataParams(null, {
        include_docs: true,
      })
    )
  ).rows.map(row => row.doc!)
}

export async function fetchMetadata(): Promise<ContextUserMetadata[]> {
  const global = await getGlobalUsers()
  const metadata = await rawUserMetadata()
  const users: ContextUserMetadata[] = []
  for (let user of global) {
    // find the metadata that matches up to the global ID
    const info = metadata.find(meta => meta._id!.includes(user._id!))
    // remove these props, not for the correct DB
    users.push({
      ...user,
      ...info,
      tableId: InternalTables.USER_METADATA,
      // make sure the ID is always a local ID, not a global one
      _id: generateUserMetadataID(user._id!),
    })
  }
  return users
}

export async function syncGlobalUsers() {
  // sync user metadata
  const dbs = [context.getDevWorkspaceDB(), context.getProdWorkspaceDB()]
  for (let db of dbs) {
    if (!(await db.exists())) {
      continue
    }
    const [users, metadata] = await Promise.all([
      getGlobalUsers(),
      rawUserMetadata(db),
    ])
    const toWrite = []
    for (let user of users) {
      const combined = combineMetadataAndUser(user, metadata)
      if (combined) {
        toWrite.push(combined)
      }
    }
    let foundEmails: string[] = []
    for (let data of metadata) {
      if (!data._id) {
        continue
      }
      const alreadyExisting =
        data.email && foundEmails.indexOf(data.email) !== -1
      const globalId = getGlobalIDFromUserMetadataID(data._id)
      if (!users.find(user => user._id === globalId) || alreadyExisting) {
        toWrite.push({ ...data, _deleted: true })
      }
      if (data.email) {
        foundEmails.push(data.email)
      }
    }
    await db.bulkDocs(toWrite)
  }
}

export function getUserContextBindings(user: ContextUser): UserBindings {
  if (!user) {
    return {}
  }

  const bindings: UserBindings = {
    _id: user._id,
    _rev: user._rev,
    firstName: user.firstName,
    lastName: user.lastName,
    email: user.email,
    status: user.status,
    roleId: user.roleId,
    globalId: user.globalId,
    userId: user.userId,
  }

  if (isSSOUser(user) && user.oauth2) {
    bindings.oauth2 = {
      accessToken: user.oauth2.accessToken,
      refreshToken: user.oauth2.refreshToken,
    }
    bindings.provider = user.provider
    bindings.providerType = user.providerType
  }

  return bindings
}
